/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import fr.x9c.cadmium.util.IO;
import fr.x9c.cadmium.util.Misc;

/**
 * This class allows reading of values from a <tt>DataInput</tt>.
 *
 * @see java.io.DataInput
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
public final class Intern implements DataFormat {

    /**
     * No instance of this class.
     */
    private Intern() {
    } // end empty constructor

    /**
     * Reads a value.
     * @param ctxt context - should not be <tt>null</tt>
     * @param in data source - should not be <tt>null</tt>
     * @param checkMagic whether magic number should be checked
     * @return read value
     * @throws IOException if an I/O error occurs
     * @throws Fail.Exception if data source is incorrect
     * @throws Fatal.Exception if data source contains unsupported elements
     *                         (such as 64-bit elements)
     */
    public static Value inputVal(final Context ctxt,
                                 final DataInput in,
                                 final boolean checkMagic)
        throws IOException, Fail.Exception, Fatal.Exception {
        assert ctxt != null : "null ctxt";
        assert in != null : "null in";
        if (checkMagic && (IO.read32s(in) != MAGIC_NUMBER)) {
            Fail.failWith("input_value: bad object");
        } // end if
        final long blockLen = IO.read32u(in);
        final long numObjects = IO.read32u(in);
        final long size32 = IO.read32u(in);
        final long size64 = IO.read32u(in);
        final List<Value> objTable = size32 != 0
            ? new ArrayList<Value>(Misc.ensure32s(numObjects))
            : null;
        return internRec(ctxt, in, objTable);
    } // end method 'inputVal(Context, DataInput, boolean)'

    /**
     * Reads a value.
     * @param ctxt context - should not be <tt>null</tt>
     * @param in data source - should not be <tt>null</tt>
     * @param objTable table of already seen values
     * @return value
     * @throws IOException if an I/O error occurs
     * @throws Fail.Exception if data source is incorrect
     * @throws Fatal.Exception if data source contains unsupported elements
     *                         (such as 64-bit elements)
     */
    private static Value internRec(final Context ctxt,
                                   final DataInput in,
                                   final List<Value> objTable)
        throws IOException, Fail.Exception, Fatal.Exception {
        assert ctxt != null : "null ctxt";
        assert in != null : "null in";
        final int code = IO.read8u(in);
        switch (code) {
        case CODE_INT8: // 0x00
            return Value.createFromLong(IO.read8s(in));
        case CODE_INT16: // 0x01
            return Value.createFromLong(IO.read16s(in));
        case CODE_INT32: // 0x02
            return Value.createFromLong(IO.read32s(in));
        case CODE_INT64: // 0x03
            Fail.failWith("input_value: integer too large");
            return null;
        case CODE_SHARED8: // 0x04
            return objTable.get(objTable.size() - IO.read8u(in));
        case CODE_SHARED16: // 0x05
            return objTable.get(objTable.size() - IO.read16u(in));
        case CODE_SHARED32: // 0x06
            return objTable.get(objTable.size() - Misc.ensure32s(IO.read32u(in)));
        case CODE_BLOCK32: // 0x08
            final int header = IO.read32s(in);
            return readBlock(ctxt,
                             in,
                             Block.tagFromHeader(header),
                             Block.wosizeFromHeader(header),
                             objTable);
        case CODE_BLOCK64: // 0x13
            Fail.failWith("input_value: data block too large");
            return null;
        case CODE_STRING8: // 0x09
            return readString(in, IO.read8u(in), objTable);
        case CODE_STRING32: // 0x0A
            return readString(in, Misc.ensure32s(IO.read32u(in)), objTable);
        case CODE_DOUBLE_LITTLE: // 0x0C
        case CODE_DOUBLE_BIG: // 0x0B
            return readDouble(in,
                              ((code != CODE_DOUBLE_BIG)
                               && (ARCH_FLOAT_ENDIANNESS == 0x76543210))
                              || ((code != CODE_DOUBLE_LITTLE)
                                  && (ARCH_FLOAT_ENDIANNESS == 0x01234567)),
                              objTable);
        case CODE_DOUBLE_ARRAY8_LITTLE: // 0x0E
        case CODE_DOUBLE_ARRAY8_BIG: // 0x0D
            return readDoubleArray(in,
                                   ((code != CODE_DOUBLE_ARRAY8_BIG)
                                    && (ARCH_FLOAT_ENDIANNESS == 0x76543210))
                                   || ((code != CODE_DOUBLE_ARRAY8_LITTLE)
                                       && (ARCH_FLOAT_ENDIANNESS == 0x01234567)),
                                   IO.read8u(in),
                                   objTable);
        case CODE_DOUBLE_ARRAY32_LITTLE: // 0x07
        case CODE_DOUBLE_ARRAY32_BIG: // 0x0F
            return readDoubleArray(in,
                                   ((code != CODE_DOUBLE_ARRAY32_BIG)
                                    && (ARCH_FLOAT_ENDIANNESS == 0x76543210))
                                   || ((code != CODE_DOUBLE_ARRAY32_LITTLE)
                                       && (ARCH_FLOAT_ENDIANNESS == 0x01234567)),
                                   Misc.ensure32s(IO.read32u(in)),
                                   objTable);
        case CODE_CODEPOINTER: // 0x10
            final long codeOfs = IO.read32s(in);
            final byte[] chkSum = new byte[16];
            in.readFully(chkSum);
            if (!Arrays.equals(ctxt.getCodeDigest(), chkSum)) {
                Fail.failWith("input_value: code mismatch");
            } // end if
            return Value.createFromCodeOffset(Misc.ensure32s(codeOfs));
        case CODE_INFIXPOINTER: // 0x11
            final int infixOfs = Misc.ensure32s(IO.read32u(in));
            final Value clos = internRec(ctxt, in, objTable);
            return clos.asBlock().get(infixOfs / 8);
        case CODE_CUSTOM: // 0x12
            final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
            byte b = in.readByte();
            while (b != 0) {
                tmp.write(b);
                b = in.readByte();
            } // end while
            final String id = Misc.convertBytesToString(tmp.toByteArray());
            tmp.close();
            final Custom.Operations ops = ctxt.findCustom(id);
            if (ops == null) {
                Fail.failWith("input_value: unknown custom block identifier");
            } // end if
            final Block tmpCustom = Block.createCustom(0, ops);
            final long customSize = ops.deserialize(in, tmpCustom);
            final Block custom = Block.createCustom(Misc.ensure32s(customSize),
                                                    ops);
            custom.setCustom(tmpCustom.asCustom());
            custom.setInt32(tmpCustom.asInt32());
            custom.setInt64(tmpCustom.asInt64());
            final Value res = Value.createFromBlock(custom);
            if (objTable != null) {
                objTable.add(res);
            } // end if
            return res;
        default:
            if ((code >= PREFIX_SMALL_STRING) && (code < PREFIX_SMALL_INT)) {
                // small string
                return readString(in, code & 0x1F, objTable);
            } else if ((code >= PREFIX_SMALL_INT) && (code < PREFIX_SMALL_BLOCK)) {
                // small integer
                return Value.createFromLong(code & 0x3F);
            } else if (code >= PREFIX_SMALL_BLOCK) {
                // small block
                return readBlock(ctxt,
                                 in,
                                 code & 0x0F,
                                 (code >> 4) & 0x07,
                                 objTable);
            } else {
                Fail.failWith("input_value: ill-formed message");
                return null; // never reached
            } // end if/elsif/else
        } // end switch
    } // end method 'internRec(Context, DataInput, List<Value>)'

    /**
     * Reads a string.
     * @param in data source - should not be <tt>null</tt>
     * @param len length of string - should be >= 0
     * @param objTable table of already seen values
     * @return string
     * @throws IOException if an I/O error occurs
     */
    private static Value readString(final DataInput in,
                                    final int len,
                                    final List<Value> objTable)
        throws IOException {
        assert in != null : "null in";
        assert len >= 0 : "len should be >= 0";
        final byte[] tmp = new byte[len];
        in.readFully(tmp);
        final Value res =
            Value.createFromBlock(Block.createString(tmp));
        if (objTable != null) {
            objTable.add(res);
        } // end if
        return res;
    } // end method 'readString(DataInput, int, List<Value>)'

    /**
     * Reads a block.
     * @param ctxt context - should not be <tt>null</tt>
     * @param in data source - should not be <tt>null</tt>
     * @param tag block tag
     * @param size block size - should be >= 0
     * @param objTable table of already seen values
     * @return block
     * @throws IOException if an I/O error occurs
     */
    private static Value readBlock(final Context ctxt,
                                   final DataInput in,
                                   final int tag,
                                   final int size,
                                   final List<Value> objTable)
        throws IOException, Fail.Exception, Fatal.Exception {
        assert ctxt != null : "null ctxt";
        assert in != null : "null in";
        assert size >= 0 : "size should be >= 0";
        if (size == 0) {
            return ctxt.getAtom(tag);
        } else {
            final Block resBlock = Block.createBlock(size, tag);
            final Value res = Value.createFromBlock(resBlock);
            if (objTable != null) {
                objTable.add(res);
            } // end if
            for (int i = 0; i < size; i++) {
                final Value v = internRec(ctxt, in, objTable);
                resBlock.set(i, v);
                if (v.isBlock() && (v.asBlock().getTag() == Block.INFIX_TAG)) {
                    v.asBlock().setParent(resBlock);
                } // end if
            } // end for
            return res;
        } // end if/else
    } // end method 'readBlock(Context, DataInput, int, int, List<Value>)'

    /**
     * Reads a double.
     * @param in data source - should not be <tt>null</tt>
     * @param toSwap whether 64-bit data read should be swapped
     * @param objTable table of already seen values
     * @return double
     * @throws IOException if an I/O error occurs
     */
    private static Value readDouble(final DataInput in,
                                    final boolean toSwap,
                                    final List<Value> objTable)
        throws IOException {
        assert in != null : "null in";
        final long doubleBits = IO.read64s(in);
        final double dbl =
            Double.longBitsToDouble(toSwap
                                    ? Long.reverseBytes(doubleBits)
                                    : doubleBits);
        final Value res = Value.createFromBlock(Block.createDouble(dbl));
        if (objTable != null) {
            objTable.add(res);
        } // end if
        return res;
    } // end method 'readDouble(DataInput, boolean, List<Value>)'

    /**
     * Reads an array of doubles.
     * @param in data source - should not be <tt>null</tt>
     * @param toSwap whether 64-bit data read should be swapped
     * @param len length of array - should be >= 0
     * @param objTable table of already seen values
     * @return array of doubles
     * @throws IOException if an I/O error occurs
     */
    private static Value readDoubleArray(final DataInput in,
                                         final boolean toSwap,
                                         final int len,
                                         final List<Value> objTable)
        throws IOException {
        assert in != null : "null in";
        assert len >= 0 : "len should be >= 0";
        final Block resBlock = Block.createDoubleArray(len);
        final Value res = Value.createFromBlock(resBlock);
        if (objTable != null) {
            objTable.add(res);
        } // end if
        for (int i = 0; i < len; i++) {
            final long doubleBits = IO.read64s(in);
            final double dbl =
                Double.longBitsToDouble(toSwap
                                        ? Long.reverseBytes(doubleBits)
                                        : doubleBits);
            resBlock.setDouble(i, dbl);
        } // end for
        return res;
    } // end method 'readDoubleArray(DataInput, boolean, int, List<Value>)'

} // end class 'Intern'
